<?php namespace sacvoyage\storage;

use \ArrayIterator;

require_once("Filter.php");
require_once("Sorter.php");
require_once("Selector.php");
require_once("Table.php");
require_once("Writer.php");
require_once("Eraser.php");
require_once("Gateway.php");

require_once('sacvoyage/Object.php');
use \sacvoyage\Object;

require_once('sacvoyage/util.php');


class Collection {

	public function __construct(Gateway &$gateway, /*string*/$prefix, /*string*/$name) {
		$this->_gateway=&$gateway; // TODO: ugly arguments(
		$this->_prefix=$prefix;
		$this->_name=$name;
	}
	
	public function getName() {
		return $this->_name;
	}

	public function write($documents) {
		$this->_ensureTables(true);
		
		// convert signle document to array for generality
		if(!is_enumerative_array($documents) && !$documents instanceof \Iterator){
			$documents=array($documents);
		}
	
		// here accumulate documents, that should be written to tables
		$write2history=array(); 
		$write2actual=array();


		foreach($documents as $document) {
			if(!is_object($document)) {
				throw new Exception(
					'sacvoyage\storage\Collection::write parameter'.
					'should be an object or an array of objects'
				);
			}
			
			if(!isset($document->_id)) {
				$document->_id=random_string(20);
			}
			$_id=$this->_gateway->escape($document->_id);
			
			if(!isset($document->_rev)) {
				$document->_rev=$this->_generateRev();
			}
			$_rev=$this->_gateway->escape($document->_rev);

			$this->_gateway->query( 
				"DELETE FROM {$this->_actualTable->getExpression()} ".
				"WHERE _id='{$_id}' AND _rev='{$_rev}'"
			);
			if($this->_gateway->getAffectedRows()>0) { 
				$write2actual[]=$document; // if exact match, just overwrite
				continue;
			} else {
				$this->_gateway->query(
					"DELETE FROM {$this->_historyTable->getExpression()} ".
					"WHERE _id='{$_id}' AND _rev='{$_rev}'"
				);
				if($this->_gateway->getAffectedRows()>0) {
					$write2history[]=$document;
					continue;
				} 	
			}
			
			$result=$this->_gateway->query(
				"SELECT _rev FROM {$this->_actualTable->getExpression()} ".
				"WHERE _id='{$_id}' LIMIT 1"
			);
			if($row=$result->fetch_row()) {
				$actual_rev=$row[0];
				if($actual_rev>$document->_rev) {
					$write2history[]=$document;
					continue;
				} else {
					$this->_gateway->query(
						"INSERT INTO {$this->_historyTable->getExpression()} ".
						"SELECT * FROM {$this->_actualTable->getExpression()} ".
						"WHERE _id='{$_id}'"
					);
					$this->_gateway->query(
						"DELETE FROM {$this->_actualTable->getExpression()} ".
						"WHERE _id='{$_id}'"
					);
				}
			} 
			$result->free();
			
			
			if(isset($document->_deleted) && $document->_deleted) {
				$write2history[]=$document;
			} else {
				$write2actual[]=$document;
			}
			
		}
		
		if($write2actual) {
			$writer=new Writer(
				$this->_actualTable, 
				new ArrayIterator($write2actual)
			);
			$writer->go();
		}
		if($write2history) {
			$writer=new Writer(
				$this->_historyTable, 
				new ArrayIterator($write2history)
			);
			$writer->go();
		}
		
		$written=array(); 
		foreach(array_merge($write2history, $write2actual) as $document) {
			$written[]=new Object(array(
				'_id' =>$document->_id, 
				'_rev'=>$document->_rev
			));
		}
	
		return $written;
	}


	/* 
	 * A shorcut to quickly retreive a whole document
	 * by its id and, optionally, rev.
	 * If no rev specified, last rev will be used.
	 * `read($id)` or `read($id, $rev)` is equvalent to 
	 * getting the first result of 
	 * `find(array('filter'=>array('_id': $id)))` or 
	 * `find(array('history'=>true, 'filter'=>array('_id': $id, '_rev': $rev)))`
	 */
	public function read($id, $rev=null) {
		$this->_ensureTables(false);
		if(!$this->_historyTable && !$this->_actualTable) {
			return null;
		}
		
		if(!is_string($id) || !(is_string($rev) || is_null($rev))) {
			throw new Exception(
				'sacvoyage\storage\Collection::read expects '.
				'parameter 1 and (optional) parameter 2 to be strings'
			);
		}
		
		if($rev) {
			$searchTables=array($this->_actualTable, $this->_historyTable);
		} else {
			$searchTables=array($this->_actualTable);
		}
		
		foreach($searchTables as $table) {
			$_id=$table->escape($id);
			if(!$rev) {
				$result=$table->query( // search in actualTable does not need selecting max rev
					"SELECT MAX(_rev) AS _rev FROM {$table->getExpression()} ".
					"WHERE _id='$_id'"
				);
				if(($row=$result->fetch_assoc()) && $row['_rev']) {
					$rev=$row['_rev'];
				} else {
					break;
				}
			}
			$keyIterator=new ArrayIterator(array(
				new Object(array('_id'=>$id, '_rev'=>$rev))
			));
			foreach(new Selector($table, $keyIterator) as $document) {
				return $document;
			}
		}
		return null;
	}

	public function find($query=null) {
		$this->_ensureTables(false);
		if(!$this->_historyTable && !$this->_actualTable) {
			return new \ArrayIterator(array());
		}
		
		if(!is_null($query) && !is_object($query)){
			throw new Exception('query must be object or null, '.gettype($query).' given');
		}
		
		if($query && isset($query->status) && $query->status=="all") {
			$view=new View(
				$this->_gateway, 
				"(SELECT * FROM {$this->_historyTable->getExpression()} ".
				"UNION ".
				"SELECT * FROM {$this->_actualTable->getExpression()})"
			);
			$includeRev=true;
		} elseif($query && isset($query->status) && $query->status=="stale") {
			$view=$this->_historyTable;
			$includeRev=true;
		} else {
			$view=$this->_actualTable;
			$includeRev=false;
		}

		$shouldLimit=false;
		if($query && isset($query->limit)) {
			$shouldLimit=true;
		}
		
		$iterator=null; //TODO: iterator cannot be empty
		
		if($query && isset($query->filter)) {
			$iterator=new Filter($view, $query->filter);
			if($shouldLimit) {
				$iterator=new Limiter($iterator, $query->limit);
				$shouldLimit=false;
			}
		}
		
		// it's a reduce-query
		if($query && isset($query->reduce)) {
			$iterator=new Reducer($view, $iterator, $query->reduce);
		} 
		
		// it's a select-query
		else {
		
			if($query && isset($query->order)) {
				$iterator=new Sorter($view, $iterator, $query->order);
				if($shouldLimit) {
					$iterator=new Limiter($iterator, $query->limit);
					$shouldLimit=false;
				}
			}
			
			// in a select-query selector runs anyway
			if($query && isset($query->select)) {
				$iterator=new Selector($view, $iterator, $query->select, $includeRev);
			} else {
				$iterator=new Selector($view, $iterator, null, $includeRev);
			}
			
			if($shouldLimit) {
				$iterator=new Limiter($iterator, $query->limit);
				$shouldLimit=false;
			}

		}
		
		return $iterator;
	}

	
	public function delete($query) {
		$this->_ensureTables(false);
		if(!$this->_historyTable && !$this->_actualTable) {
			return 0;
		}
		
		if(!$query) {
			$query=new stdObject();
		}
		if(!isset($query->filter)) {
			$query->filter=null;
		}
		
		// TODO: !!!!!!!!!!!!
		if(isset($query->status) 
		&&($query->status=="all" || $query->status=="stale")) {
			return $this->_purge($query->filter, $query->status);
		} else { // "actual" or absent
			return $this->_markDeleted($query->filter);
		}
	}
	

	private $_gateway;
	private $_userName;
	private $_name;
	
	private $_actualTable;
	private $_historyTable;
	
	private function _markDeleted($filter) {
		$documents=array();
		foreach(new Filter($this->_actualTable, $filter) as $document) {
			$document->_deleted=true;
			unset($document->_rev);
			$documents[]=$document;
		}
		return $this->write($documents);
	}
	
	private function _purge($filter=null, $status="all") { // status in ["all", "history"]
		$count=0;
		
		// in any case purge document from history, it's easy
		$filterObj=$filter ? new Filter($this->_historyTable, $filter) : null;
		$eraser=new Eraser(
			$this->_historyTable,
			$filterObj
		);
		$count+=$eraser->go();
		
		if($status=="all") { // if "all", actual must be purged too
			
			//
			$keys=iterator_to_array(new Filter($this->_actualTable, $filter));
			$eraser=new Eraser(
				$this->_actualTable,
				new ArrayIterator($keys)
			);
			$count+=$eraser->go();
			
			foreach($keys as $key) {
				$_id=$this->_actualTable->escape($key->_id);
			
				$result=$this->_actualTable->query(
					"SELECT MAX(_rev) AS _rev ".
					"FROM {$this->_historyTable->getExpression()} ".
					"WHERE _id='$_id'"
				);	
				if($row=$result->fetch_assoc()) {
					$rev=$row['_rev'];
					$_rev=$this->_actualTable->escape($rev);
					$this->_gateway->query(
						"INSERT INTO {$this->_actualTable->getExpression()} ".
						"SELECT * FROM {$this->_historyTable->getExpression()} ".
						"WHERE _id='{$_id}' AND _rev='{$_rev}'"
					);	
					$this->_gateway->query(
						"DELETE FROM {$this->_historyTable->getExpression()} ".
						"WHERE _id='{$_id}'"
					);
				}
				$result->free();
			}
		}
		
		$this->_deleteIfEmpty();
		return $count;
	}
	
	private function _writeSingleDocument($table, $document) {
		$writer=new Writer(
			$table, 
			new ArrayIterator(array($document))
		);
		$writer->go();
	}


	private function _generateRev() {
		return date('Ymd\THis').'-'.random_string(4);
	}


	/*
	 * ensures that all tables for this storage exist, create if needed
	 */
	private function _ensureTables($shouldCreate) {
		if(!$this->_historyTable || !$this->_actualTable) {
			$name=$this->_gateway->escape($this->_name);
			$result=$this->_gateway->query(
				"SELECT actual_table, history_table ".
				"FROM {$this->_prefix}collections ".
				"WHERE name='$name' ".
				"LIMIT 1"
			);
			if($row=$result->fetch_assoc()) { // already exist
				$this->_actualTable=new Table($this->_gateway, $row['actual_table']);
				$this->_historyTable=new Table($this->_gateway, $row['history_table']);
			} elseif($shouldCreate) { // not exist, but should be created
				$actualTableName=$this->_gateway->generateTableName(
					$this->_prefix.str_replace('.', '_', $this->_name).'_actual',
					$this->_prefix, '_actual'
				);
				$historyTableName=$this->_gateway->generateTableName(
					$this->_prefix.str_replace('.', '_', $this->_name).'_history',
					$this->_prefix, '_history'
				);
				
				$this->_gateway->query(
					"INSERT INTO {$this->_prefix}collections (name, actual_table, history_table)".
					"VALUES ( '$name' , '$actualTableName', '$historyTableName')"
				);
					
				foreach(array($actualTableName, $historyTableName) as $tableName) {
					$this->_gateway->query("CREATE TABLE $tableName (
		_id       TEXT NOT NULL, 
		_rev      TEXT NOT NULL,
		path	    TEXT NOT NULL,
		type      SET('string', 'number', 'object', 'array', 'boolean', 'null', 'missing') NOT NULL,
		value MEDIUMTEXT, -- up to 8388608
		INDEX(_id(16)), INDEX(_rev(20)), INDEX(path(25)), INDEX(type), INDEX(value(25))
	) DEFAULT CHARSET=utf8 COLLATE=utf8_bin");
				}
	
				$this->_actualTable=new Table($this->_gateway, $actualTableName);
				$this->_historyTable=new Table($this->_gateway, $historyTableName);
			} else { // not exists and should not be created
				$this->_actualTable=null;
				$this->_historyTable=null;				
			}
		} // create tables
	
	}

	private function _deleteIfEmpty() {
		$this->_ensureTables(false);
		if(!$this->_historyTable && !$this->_actualTable) {
			return;
		}
		
		$result=$this->_gateway->query(
			"SELECT * FROM {$this->_historyTable->getExpression()} LIMIT 1 ".
			"UNION ".
			"SELECT * FROM {$this->_actualTable->getExpression()} LIMIT 1"
		);
		if($row=$result->fetch_row()) {
			return;
		} else {
			$name=$this->_gateway->escape($this->_name);
			$this->_gateway->query(
				"DELETE FROM {$this->_prefix}collections ".
				"WHERE name='$name'"
			);
			
			$this->_historyTable->drop();
			$this->_historyTable=null;
			
			$this->_actualTable->drop();
			$this->_actualTable=null;
			
		}
	}
}

?>
